#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of Beremiz, a Integrated Development Environment for
# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
#
# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
# Copyright (C) 2017: Andrey Skvortsov
#
# See COPYING file for copyrights details.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
Beremiz Project Controller
"""
import asyncio
import hashlib
import locale
import os
import re
import shutil
import socket
import tempfile
import time
import traceback
from datetime import datetime
from distutils.dir_util import copy_tree
from functools import reduce
from os.path import exists
from weakref import WeakKeyDictionary

import wx
from loguru import logger
from serial import SerialException
from wxasync import StartCoroutine

from proto.firmata_pb2 import Stopped, Started, Empty, Broken, Disconnected, STATE_POWER_FAULT, pb_state, CB_PLC_START, \
    CB_PLC_STOP, CB_PLC_REPAIR, CB_PLC_LOAD, pb_soem_cmd, pb_soem_cmd_type
from proto.firmata_pb2 import pb_cmd_type
from resources import GetBitmap
from . import features, targets
from .ConfigTreeNode import ConfigTreeNode, XSDSchemaErrorMessage
from .PLCControler import PLCControler
from .PLCGenerator import ProgramGenerator
from .POULibrary import UserAddressedException
from .connectors import ConnectorFactory
from .dialogs.FwExport import UCopyEditor
from .dialogs.IDManager import IDManager
from .dialogs.Packager import PackageDialog
from .dialogs.ProgEditor import ProgEditor
from .dialogs.UriEditor import UriEditor
from .editors.DebugViewer import DebugViewer, REFRESH_PERIOD
from .editors.FileManagementPanel import FileManagementPanel
from .editors.IECCodeViewer import IECCodeViewer
from .editors.ProjectNodeEditor import ProjectNodeEditor
from .plcopen import plcopen, PLCOpenParser
from .plcopen.BlockInstanceCollector import BlockInstanceCollector
from .plcopen.InstanceTagnameCollector import InstanceTagnameCollector
from .plcopen.InstancesPathCollector import InstancesPathCollector
from .plcopen.POUVariablesCollector import POUVariablesCollector
from .plcopen.VariableInfoCollector import VariableInfoCollector
from .plcopen.structures import IEC_KEYWORDS
from .plcopen.types_enums import ComputeConfigurationResourceName, ITEM_CONFNODE
from .runtime.typemapping import DebugTypesSize, UnpackDebugBuffer
from .util.MiniTextControler import MiniTextControler
from .util.ProcessLogger import ProcessLogger
from .util.misc import CheckPathPerm, GetClassImporter

_ = wx.GetTranslation
PLC_CONTROLER = None
loop = asyncio.get_event_loop()


def get_host_ip():
    s = None
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
    finally:
        if s:
            s.close()
    return ip


base_folder = os.path.dirname(__file__)

MATIEC_ERROR_MODEL = re.compile(
    r".*\.st:(\d+)-(\d+)\.\.(\d+)-(\d+): (?:error)|(?:warning) : (.*)$")


def ExtractChildrenTypesFromCatalog(catalog):
    children_types = []
    for n, d, _h, c in catalog:
        if isinstance(c, list):
            children_types.extend(ExtractChildrenTypesFromCatalog(c))
        else:
            children_types.append((n, GetClassImporter(c), d))
    return children_types


def ExtractMenuItemsFromCatalog(catalog):
    menu_items = []
    for n, d, h, c in catalog:
        if isinstance(c, list):
            children = ExtractMenuItemsFromCatalog(c)
        else:
            children = []
        menu_items.append((n, d, h, children))
    return menu_items


def GetAddMenuItems():
    return ExtractMenuItemsFromCatalog(features.catalog)


class Iec2CSettings(object):

    def __init__(self, controler):
        self.iec2c = None
        self.iec2c_buildopts = ' -f -l -p'
        self.ieclib_path = self.findLibPath()
        self.ieclib_c_path = self.findLibCPath()
        self.controler = controler

    def findObject(self, paths, test):
        path = None
        for p in paths:
            if test(p):
                path = p
                break
        return path

    def findCmd(self):
        cmd = "iec2c" + (".exe" if wx.Platform == '__WXMSW__' else "")
        paths = [
            os.path.join(base_folder, "matiec"),
            os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "matiec")),
            "/usr/lib/matiec",
        ]
        path = self.findObject(
            paths, lambda p: os.path.isfile(os.path.join(p, cmd)))

        # otherwise use iec2c from PATH
        if path is not None:
            cmd = os.path.join(path, cmd)

        return cmd

    def findLibPath(self):
        paths = [
            os.path.join(base_folder, "matiec", "lib"),
            os.path.realpath(os.path.join(os.path.dirname(__file__), '..', "matiec", "lib")),
            "/usr/lib/matiec",
        ]
        path = self.findObject(
            paths, lambda p: os.path.isfile(os.path.join(p, "ieclib.txt")))
        return path

    def findLibCPath(self):
        path = None
        if self.ieclib_path is not None:
            paths = [
                os.path.join(self.ieclib_path, "C"),
                self.ieclib_path]
            path = self.findObject(
                paths,
                lambda p: os.path.isfile(os.path.join(p, "iec_types.h")))
        return path

    def findSupportedOptions(self):
        buildcmd = "\"%s\" -h" % (self.getCmd())
        options = ["-f", "-l", "-p"]

        buildopt = ""
        try:
            # Invoke compiler.
            # Output files are listed to stdout, errors to stderr
            _status, result, _err_result = ProcessLogger(self.controler.logger, buildcmd,
                                                         no_stdout=True,
                                                         no_stderr=True, output_encoding='utf-8').spin()
        except Exception:
            self.controler.logger.error(_("Couldn't launch IEC compiler to determine compatible options.\n"))
            return buildopt

        for opt in options:
            if opt in result:
                buildopt = buildopt + " " + opt
        return buildopt

    def getCmd(self):
        if self.iec2c is None:
            self.iec2c = self.findCmd()
        return self.iec2c

    def getOptions(self):
        if self.iec2c_buildopts is None:
            self.iec2c_buildopts = self.findSupportedOptions()
        return self.iec2c_buildopts

    def getLibPath(self):
        return self.ieclib_path

    def getLibCPath(self):
        if self.ieclib_c_path is None:
            self.ieclib_c_path = self.findLibCPath()
        return self.ieclib_c_path


def GetProjectControllerXSD():
    XSD = """<?xml version="1.0" encoding="utf-8" ?>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <xsd:element name="BeremizRoot">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="TargetType">
              <xsd:complexType>
                <xsd:choice minOccurs="0">
                """ + targets.GetTargetChoices() + """
                </xsd:choice>
              </xsd:complexType>
            </xsd:element>""" + (("""
            <xsd:element name="Libraries" minOccurs="0">
              <xsd:complexType>
              """ + "\n".join(['<xsd:attribute name=' +
                               '"Enable_' + libname + '_Library" ' +
                               'type="xsd:boolean" use="optional" default="' +
                               ('true' if default else 'false') + '"/>'
                               for libname, _lib, default in features.libraries]) + """
              </xsd:complexType>
            </xsd:element>""") if len(features.libraries) > 0 else '') + """
          </xsd:sequence>
          <xsd:attribute name="Disable_Extensions" type="xsd:boolean" use="optional" default="false"/>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
    """
    return XSD


class ProjectController(ConfigTreeNode, PLCControler):
    """
    This class define Root object of the confnode tree.
    It is responsible of :
    - Managing project directory
    - Building project
    - Handling PLCOpenEditor controler and view
    - Loading user confnodes and instanciante them as children
    - ...

    """
    # For root object, available Children Types are modules of the confnode
    # packages.
    CTNChildrenTypes = ExtractChildrenTypesFromCatalog(features.catalog)
    XSD = GetProjectControllerXSD()
    EditorType = ProjectNodeEditor
    iec2c_cfg = None

    def __init__(self, frame, logger):
        self.BeremizRoot = None
        global PLC_CONTROLER
        self.AppFrame = None
        self.logger = None
        PLCControler.__init__(self)
        ConfigTreeNode.__init__(self)

        if self.iec2c_cfg is None:
            self.iec2c_cfg = Iec2CSettings(self)

        self.MandatoryParams = None
        self._builder = None
        self._connector = None
        self.DispatchDebugValuesTimer = None
        self.DebugValuesBuffers = []
        self.DebugTicks = []
        self.SetAppFrame(frame, logger)

        # Setup debug information
        self.IECdebug_datas = {}
        self.IECdebug_force = {}
        self.DebugUpdatePending = False
        self.ResetIECProgramsAndVariables()

        # In both new or load scenario, no need to save
        self.ChangesToSave = False
        # root have no parent
        self.CTNParent = None
        # Keep track of the confnode type name
        self.CTNType = "Beremiz"
        self.Children = {}
        self._View = None
        # After __init__ root confnode is not valid
        self.ProjectPath = None
        self._setBuildPath(None)
        self.debug_break = False
        self.previous_plcstate = None
        # copy StatusMethods so that it can be later customized
        self.StatusMethods = [dic.copy() for dic in self.StatusMethods]
        PLC_CONTROLER = self
        self.DebugToken = None
        self.LastComplainDebugToken = None
        self.debug_status = Stopped
        self.rts = None
        self.dtr = None
        self.InstancesPathCollector = InstancesPathCollector(self)
        self.POUVariablesCollector = POUVariablesCollector(self)
        self.InstanceTagnameCollector = InstanceTagnameCollector(self)
        self.BlockInstanceCollector = BlockInstanceCollector(self)
        self.VariableInfoCollector = VariableInfoCollector(self)
        self.IECcodeDigest = None
        self.LastBuiltIECcodeDigest = None
        self.reg_var_nrs = 50

    def __del__(self):
        self.KillDebugThread()

    def LoadLibraries(self):
        self.Libraries = []
        TypeStack = []
        for libname, clsname, lib_enabled in features.libraries:
            if self.BeremizRoot.Libraries is not None:
                enable_attr = getattr(self.BeremizRoot.Libraries,
                                      "Enable_" + libname + "_Library")
                if enable_attr is not None:
                    lib_enabled = enable_attr

            if lib_enabled:
                Lib = GetClassImporter(clsname)()(self, libname, TypeStack)
                TypeStack.append(Lib.GetTypes())
                self.Libraries.append(Lib)

    def SetAppFrame(self, frame, logger):
        self.AppFrame = frame
        self.logger = logger
        if self.DispatchDebugValuesTimer is not None:
            self.DispatchDebugValuesTimer.Stop()
        self.DispatchDebugValuesTimer = None

        if frame is not None:
            # Timer to pull PLC status

            # if self._connector is not None:
            #     frame.LogViewer.SetLogSource(self._connector)
            #     asyncio.run(self.PullPLCStatusProc())

            # Timer to dispatch debug values to consumers
            self.DispatchDebugValuesTimer = wx.Timer(self.AppFrame, -1)
            self.AppFrame.Bind(wx.EVT_TIMER, self.DispatchDebugValuesProc, self.DispatchDebugValuesTimer)

            asyncio.run(self.RefreshConfNodesBlockLists())

    def ResetAppFrame(self, logger):
        if self.AppFrame is not None:
            StartCoroutine(self.CloseConnector, self.AppFrame)
            self.AppFrame = None
            self.KillDebugThread()
        self.logger = logger

    def CTNName(self):
        return "Project"

    def CTNTestModified(self):
        return self.ChangesToSave or not self.ProjectIsSaved()

    def CTNFullName(self):
        return ""

    def GetCTRoot(self):
        return self

    def GetIECLibPath(self):
        return self.iec2c_cfg.getLibCPath()

    def GetIEC2cPath(self):
        return self.iec2c_cfg.getCmd()

    def GetCurrentLocation(self):
        return ()

    def GetCurrentName(self):
        return ""

    def _GetCurrentName(self):
        return ""

    def GetProjectPath(self):
        return self.ProjectPath

    def GetProjectName(self, debug=False) -> str:
        if self.ProjectPath:
            return os.path.split(self.ProjectPath)[1]
        return 'Empty'

    def GetIconName(self):
        return "PROJECT"

    def GetDefaultTargetName(self):
        if wx.Platform == '__WXMSW__':
            return "Win32"
        else:
            return "Linux"

    def GetTarget(self):
        target = self.BeremizRoot.getTargetType()
        if target.getcontent() is None:
            temp_root = self.Parser.CreateRoot()
            target = self.Parser.CreateElement("TargetType", "BeremizRoot")
            temp_root.setTargetType(target)
            target_name = self.GetDefaultTargetName()
            target.setcontent(
                self.Parser.CreateElement(target_name, "TargetType"))
        return target

    def GetParamsAttributes(self, path=None):
        params = ConfigTreeNode.GetParamsAttributes(self, path)
        if params[0]["name"] == "BeremizRoot":
            for child in params[0]["children"]:
                if child["name"] == "TargetType" and child["value"] == '':
                    child.update(
                        self.GetTarget().getElementInfos("TargetType"))
        return params

    def SetParamsAttribute(self, path, value):
        if path.startswith("BeremizRoot.TargetType.") and self.BeremizRoot.getTargetType().getcontent() is None:
            self.BeremizRoot.setTargetType(self.GetTarget())
        res = ConfigTreeNode.SetParamsAttribute(self, path, value)
        if path.startswith("BeremizRoot.Libraries."):
            StartCoroutine(self.RefreshConfNodesBlockLists, self.AppFrame)
        return res

    # helper func to check project path write permission
    def CheckProjectPathPerm(self):
        if CheckPathPerm(self.ProjectPath):
            return True
        if self.AppFrame is not None:
            dialog = wx.MessageDialog(
                self.AppFrame,
                _('You must have permission to work on the project\nWork on a project copy ?'),
                _('Error'),
                wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
            answer = dialog.ShowModal()
            dialog.Destroy()
            if answer == wx.ID_YES:
                if self.SaveProjectAs():
                    self.AppFrame.RefreshTitle()
                    self.AppFrame.RefreshFileMenu()
                    self.AppFrame.RefreshPageTitles()
                    return True
        return False

    def _getProjectFilesPath(self, project_path=None):
        if project_path is not None:
            return os.path.join(project_path, "project_files")
        projectfiles_path = os.path.join(
            self.GetProjectPath(), "project_files")
        if not os.path.exists(projectfiles_path):
            os.mkdir(projectfiles_path)
        return projectfiles_path

    def AddProjectDefaultConfiguration(self, config_name="config", res_name="resource1"):
        self.ProjectAddConfiguration(config_name)
        self.ProjectAddConfigurationResource(config_name, res_name)

    def SetProjectDefaultConfiguration(self):
        # Sets default task and instance for new project
        config = self.Project.getconfiguration(
            self.GetProjectMainConfigurationName())
        resource = config.getresource()[0].getname()
        config = config.getname()
        resource_tagname = ComputeConfigurationResourceName(config, resource)
        def_task = [
            {'Priority': '0', 'Single': '', 'Interval': 'T#20ms', 'Name': 'task0', 'Triggering': 'Cyclic'}]
        def_instance = [
            {'Task': def_task[0].get('Name'), 'Type': self.GetProjectPouNames()[0], 'Name': 'instance0'}]
        self.SetEditedResourceInfos(resource_tagname, def_task, def_instance)

    async def NewProject(self, ProjectPath, BuildPath=None):
        """
        Create a new project in an empty folder
        @param ProjectPath: path of the folder where project have to be created
        """
        # Verify that chosen folder is empty
        if not os.path.isdir(ProjectPath) or len(os.listdir(ProjectPath)) > 0:
            return _("Chosen folder isn't empty. You can't use it for a new project!")

        # Create PLCOpen program
        creationDateTime = datetime.now()
        self.CreateNewProject(
            {"projectName": _("Unnamed"),
             "productName": _("Unnamed"),
             "productVersion": "1",
             "companyName": _("Unknown"),
             "creationDateTime": creationDateTime})
        self.AddProjectDefaultConfiguration()

        # Change XSD into class members
        self._AddParamsMembers()
        self.Children = {}
        # Keep track of the root confnode (i.e. project path)
        self.ProjectPath = ProjectPath
        self._setBuildPath(BuildPath)
        # get confnodes bloclist (is that usefull at project creation?)
        await self.RefreshConfNodesBlockLists()
        # this will create files base XML files
        self.SaveProject()
        return None

    def LoadProject(self, ProjectPath, BuildPath=None):
        """
        Load a project contained in a folder
        @param ProjectPath: path of the project folder
        """
        if os.path.basename(ProjectPath) == "":
            ProjectPath = os.path.dirname(ProjectPath)
        # Verify that project contains a PLCOpen program
        plc_file = os.path.join(ProjectPath, "plc.xml")
        if not os.path.isfile(plc_file):
            return _("Chosen folder doesn't contain a program. It's not a valid project!"), True
        # Load PLCOpen file
        try:
            error = self.OpenXMLFile(plc_file)
            if error is not None:
                if self.Project is not None:
                    (fname_err, lnum, src) = (("PLC",) + error)
                    self.logger.warning(
                        XSDSchemaErrorMessage.format(a1=fname_err, a2=lnum, a3=src))
                else:
                    return error, False
        except Exception as e:
            logger.error(traceback.format_exc())
            return _("Error while loading project: ") + str(e), False
        if len(self.GetProjectConfigNames()) == 0:
            self.AddProjectDefaultConfiguration()
        # Change XSD into class members
        self._AddParamsMembers()
        self.Children = {}
        # Keep track of the root confnode (i.e. project path)
        self.ProjectPath = ProjectPath
        self._setBuildPath(BuildPath)
        # If dir have already be made, and file exist
        if os.path.isdir(self.CTNPath()) and os.path.isfile(self.ConfNodeXmlFilePath()):
            # Load the confnode.xml file into parameters members
            result = self.LoadXMLParams()
            if result:
                return result, False
            # Load and init all the children
        self.LoadChildren()
        asyncio.run(self.RefreshConfNodesBlockLists())
        if self.AppFrame is not None:
            self.UpdateButtons()
        return None, False

    def RecursiveConfNodeInfos(self, confnode):
        values = []
        for CTNChild in confnode.IECSortedChildren():
            values.append(
                {"name": "%s: %s" % (CTNChild.GetFullIEC_Channel(),
                                     CTNChild.CTNName()),
                 "tagname": CTNChild.CTNFullName(),
                 "type": ITEM_CONFNODE,
                 "confnode": CTNChild,
                 "icon": CTNChild.GetIconName(),
                 "values": self.RecursiveConfNodeInfos(CTNChild)})
        return values

    def GetProjectInfos(self):
        infos = PLCControler.GetProjectInfos(self)
        configurations = infos["values"].pop(-1)
        resources = None
        for config_infos in configurations["values"]:
            if resources is None:
                resources = config_infos["values"][0]
            else:
                resources["values"].extend(config_infos["values"][0]["values"])
        if resources is not None:
            infos["values"].append(resources)
        infos["values"].extend(self.RecursiveConfNodeInfos(self))
        return infos

    def CloseProject(self):
        self.ClearChildren()
        self.ResetAppFrame(None)

    def CheckNewProjectPath(self, old_project_path, new_project_path):
        if old_project_path == new_project_path:
            message = (_("Save path is the same as path of a project! \n"))
            dialog = wx.MessageDialog(
                self.AppFrame, message, _("Error"), wx.OK | wx.ICON_ERROR)
            dialog.ShowModal()
            return False
        else:
            if not CheckPathPerm(new_project_path):
                dialog = wx.MessageDialog(
                    self.AppFrame,
                    _('No write permissions in selected directory! \n'),
                    _("Error"), wx.OK | wx.ICON_ERROR)
                dialog.ShowModal()
                return False
            if not os.path.isdir(new_project_path) or len(os.listdir(new_project_path)) > 0:
                plc_file = os.path.join(new_project_path, "plc.xml")
                if os.path.isfile(plc_file):
                    message = _("Selected directory already contains another project. Overwrite? \n")
                else:
                    message = _("Selected directory isn't empty. Continue? \n")
                dialog = wx.MessageDialog(
                    self.AppFrame, message, _("Error"), wx.YES_NO | wx.ICON_ERROR)
                answer = dialog.ShowModal()
                return answer == wx.ID_YES
        return True

    def SaveProject(self, from_project_path=None):
        if self.CheckProjectPathPerm():
            if from_project_path is not None:
                old_projectfiles_path = self._getProjectFilesPath(
                    from_project_path)
                if os.path.isdir(old_projectfiles_path):
                    copy_tree(old_projectfiles_path,
                              self._getProjectFilesPath(self.ProjectPath))
            self.SaveXMLFile(os.path.join(self.ProjectPath, 'plc.xml'))
            result = self.CTNRequestSave(from_project_path)
            if result:
                self.logger.error(result)

    def SaveProjectAs(self):
        # Ask user to choose a path with write permissions
        if wx.Platform == '__WXMSW__':
            path = os.getenv("USERPROFILE")
        else:
            path = os.getenv("HOME")
        dirdialog = wx.DirDialog(
            self.AppFrame, _("Create or choose an empty directory to save project"), path, wx.DD_NEW_DIR_BUTTON)
        answer = dirdialog.ShowModal()
        newprojectpath = dirdialog.GetPath()
        dirdialog.Destroy()
        if answer == wx.ID_OK:
            if os.path.isdir(newprojectpath):
                if self.CheckNewProjectPath(self.ProjectPath, newprojectpath):
                    self.ProjectPath, old_project_path = newprojectpath, self.ProjectPath
                    self.SaveProject(old_project_path)
                    self._setBuildPath(self.BuildPath)
                return True
        return False

    def GetLibrariesTypes(self):
        self.LoadLibraries()
        return [lib.GetTypes() for lib in self.Libraries]

    def GetLibrariesSTCode(self):
        return "\n".join([lib.GetSTCode() for lib in self.Libraries])

    def GetLibrariesCCode(self, buildpath):
        if len(self.Libraries) == 0:
            return [], [], ()
        self.GetIECProgramsAndVariables()
        LibIECCflags = '"-I%s" -Wno-unused-function' % os.path.abspath(
            self.GetIECLibPath())
        LocatedCCodeAndFlags = []
        Extras = []
        for lib in self.Libraries:
            res = lib.Generate_C(buildpath, self._VariablesList, LibIECCflags)
            LocatedCCodeAndFlags.append(res[:2])
            if len(res) > 2:
                Extras.extend(res[2:])
        return list(map(list, list(zip(*LocatedCCodeAndFlags)))) + [tuple(Extras)]

    # Update PLCOpenEditor ConfNode Block types from loaded confnodes
    async def RefreshConfNodesBlockLists(self):
        if getattr(self, "Children", None) is not None:
            self.ClearConfNodeTypes()
            self.AddConfNodeTypesList(self.GetLibrariesTypes())
        if self.AppFrame is not None:
            self.AppFrame.RefreshLibraryPanel()
            self.AppFrame.RefreshEditor()

    # Update a PLCOpenEditor Pou variable location
    async def UpdateProjectVariableLocation(self, old_leading, new_leading):
        self.Project.updateElementAddress(old_leading, new_leading)
        self.BufferProject()
        if self.AppFrame is not None:
            self.AppFrame.RefreshTitle()
            self.AppFrame.RefreshPouInstanceVariablesPanel()
            self.AppFrame.RefreshFileMenu()
            self.AppFrame.RefreshEditMenu()
            wx.CallAfter(self.AppFrame.RefreshEditor)

    def GetVariableLocationTree(self):
        '''
        This function is meant to be overridden by confnodes.

        It should returns an list of dictionaries

        - IEC_type is an IEC type like BOOL/BYTE/SINT/...
        - location is a string of this variable's location, like "%IX0.0.0"
        '''
        children = []
        for child in self.IECSortedChildren():
            try:
                children.append(child.GetVariableLocationTree())
            except Exception:
                logger.error(traceback.format_exc())
        return children

    def CTNPath(self, CTNName: object = None, project_path=None) -> object:
        return self.ProjectPath

    def ConfNodeXmlFilePath(self, CTNName=None):
        return os.path.join(self.CTNPath(CTNName), "beremiz.xml")

    def ParentsTypesFactory(self):
        return self.ConfNodeTypesFactory()

    def _setBuildPath(self, buildpath):
        self.BuildPath = buildpath
        self.DefaultBuildPath = None
        if self._builder is not None:
            self._builder.SetBuildPath(self._getBuildPath())

    def _getBuildPath(self):
        # BuildPath is defined by user
        if self.BuildPath is not None:
            return self.BuildPath
        # BuildPath isn't defined by user but already created by default
        if self.DefaultBuildPath is not None:
            return self.DefaultBuildPath
        # Create a build path in project folder if user has permissions
        if CheckPathPerm(self.ProjectPath):
            self.DefaultBuildPath = os.path.join(self.ProjectPath, "src")
        # Create a build path in temp folder
        else:
            self.DefaultBuildPath = os.path.join(
                tempfile.mkdtemp(), os.path.basename(self.ProjectPath), "src")

        if not os.path.exists(self.DefaultBuildPath):
            os.makedirs(self.DefaultBuildPath)
        return self.DefaultBuildPath

    def _getExtraFilesPath(self):
        return os.path.join(self._getBuildPath(), "extra_files")

    def _getIECcodepath(self):
        # define name for IEC code file
        return os.path.join(self._getBuildPath(), "plc.st")

    def _getIECgeneratedcodepath(self):
        # define name for IEC generated code file
        return os.path.join(self._getBuildPath(), "generated_plc.st")

    def _getIECrawcodepath(self):
        # define name for IEC raw code file
        return os.path.join(self.CTNPath(), "raw_plc.st")

    def GetLocations(self):
        locations = []
        filepath = os.path.join(self._getBuildPath(), "LOCATED_VARIABLES.h")
        if os.path.isfile(filepath):
            # IEC2C compiler generate a list of located variables :
            # LOCATED_VARIABLES.h
            location_file = open(
                os.path.join(self._getBuildPath(), "LOCATED_VARIABLES.h"), encoding='utf-8')
            # each line of LOCATED_VARIABLES.h declares a located variable
            lines = [line.strip() for line in location_file.readlines()]
            # This regular expression parses the lines genereated by IEC2C
            LOCATED_MODEL = re.compile(
                r"__LOCATED_VAR\((?P<IEC_TYPE>[A-Z]*),(?P<NAME>[_A-Za-z0-9]*),(?P<DIR>[QMI])(?:,(?P<SIZE>[XBWDL]))?,(?P<LOC>[,0-9]*)\)")
            for line in lines:
                # If line match RE,
                result = LOCATED_MODEL.match(line)
                if result:
                    # Get the resulting dict
                    resdict = result.groupdict()
                    # rewrite string for variadic location as a tuple of
                    # integers
                    resdict['LOC'] = tuple(map(int, resdict['LOC'].split(',')))
                    # set located size to 'X' if not given
                    if not resdict['SIZE']:
                        resdict['SIZE'] = 'X'
                    # finally store into located variable list
                    locations.append(resdict)
        return locations

    def GetConfNodeGlobalInstances(self):
        LibGlobals = []
        for lib in self.Libraries:
            LibGlobals += lib.GlobalInstances()
        CTNGlobals = self._GlobalInstances()
        return LibGlobals + CTNGlobals

    async def _Generate_SoftPLC(self):
        if await self._Generate_PLC_ST():
            return await self._Compile_ST_to_SoftPLC()
        return False

    async def _Generate_PLC_ST(self):
        """
        Generate SoftPLC ST/IL/SFC code out of PLCOpenEditor controller, and compile it with IEC2C
        @param buildpath: path where files should be created
        """

        # Update PLCOpenEditor ConfNode Block types before generate ST code
        await self.RefreshConfNodesBlockLists()

        self.logger.info(
            _("Generating SoftPLC IEC-61131 ST/IL/SFC code...\n"))
        # ask PLCOpenEditor controller to write ST/IL/SFC code file
        _program, errors, warnings = self.GenerateProgramC(
            self._getIECgeneratedcodepath(), self.logger)
        if len(warnings) > 0:
            self.logger.warning(
                _("Warnings in ST/IL/SFC code generator :\n"))
            for warning in warnings:
                self.logger.warning("%s\n" % warning)
        if len(errors) > 0:
            # Failed !
            self.logger.error(
                _("Error in ST/IL/SFC code generator :\n%s\n") % errors[0])
            return False

        # Add ST Library from confnodes
        IECCodeContent = self.GetLibrariesSTCode()

        IECrawcodepath = self._getIECrawcodepath()
        if os.path.isfile(IECrawcodepath):
            IECCodeContent += open(IECrawcodepath, "r", encoding='utf-8').read() + "\n"

        # Compute offset before ST resulting of transformation from user POUs
        self.ProgramOffset = IECCodeContent.count("\n")

        POUsIECCodeContent = open(self._getIECgeneratedcodepath(), "r", encoding='utf-8').read()

        IECcodepath = self._getIECcodepath()

        if not os.path.exists(IECcodepath):
            self.LastBuiltIECcodeDigest = None

        with open(IECcodepath, "w", encoding='utf-8') as plc_file:
            plc_file.write(IECCodeContent)
            plc_file.write(POUsIECCodeContent)

        hasher = hashlib.md5()
        hasher.update(IECCodeContent.encode())
        hasher.update(POUsIECCodeContent.encode())
        self.IECcodeDigest = hasher.hexdigest()

        return True

    async def _Compile_ST_to_SoftPLC(self):
        iec2c_libpath = self.iec2c_cfg.getLibPath()
        if iec2c_libpath is None:
            self.logger.error(_("matiec installation is not found\n"))
            return False

        if self.LastBuiltIECcodeDigest == self.IECcodeDigest:
            self.logger.write(_("IEC program did no change, not re-compiling into C code.\n"))
            return True

        self.logger.info(_("Compiling IEC Program into C code...\n"))
        buildpath = self._getBuildPath()
        buildcmd = "\"%s\" %s -I \"%s\" -T \"%s\" \"%s\"" % (
            self.iec2c_cfg.getCmd(),
            self.iec2c_cfg.getOptions(),
            iec2c_libpath,
            buildpath,
            self._getIECcodepath())

        try:
            # Invoke compiler.
            # Output files are listed to stdout, errors to stderr
            output_encoding = locale.getdefaultlocale()[1]
            status, result, err_result = ProcessLogger(self.logger, buildcmd,
                                                       no_stdout=True,
                                                       no_stderr=True, output_encoding='utf-8').spin()
        except Exception as e:
            print(traceback.format_exc())
            self.logger.error(buildcmd + "\n")
            self.logger.error(repr(e) + "\n")
            return False

        if status:
            # Failed !

            # parse iec2c's error message. if it contains a line number,
            # then print those lines from the generated IEC file.
            for err_line in err_result.split('\n'):
                self.logger.warning(err_line + "\n")

                m_result = MATIEC_ERROR_MODEL.match(err_line)
                if m_result is not None:
                    first_line, _first_column, last_line, _last_column, _error = m_result.groups()
                    first_line, last_line = int(first_line), int(last_line)

                    last_section = None
                    f = open(self._getIECcodepath(), encoding='utf-8')

                    for i, line in enumerate(f.readlines()):
                        i = i + 1
                        if line[0] not in '\t \r\n':
                            last_section = line

                        if first_line <= i <= last_line:
                            if last_section is not None:
                                self.logger.warning(
                                    "In section: " + last_section)
                                last_section = None  # only write section once
                            self.logger.warning("%04d: %s" % (i, line))

                    f.close()

            self.logger.error(
                _("Error : IEC to C compiler returned %d\n") % status)
            return False

        h_file = os.path.join(buildpath, "LOCATED_VARIABLES.h")
        self.sortVariblesH(h_file)
        # Now extract C files of stdout
        C_files = [fname for fname in result.splitlines() if fname[
                                                             -2:] == ".c" or fname[-2:] == ".C"]
        # remove those that are not to be compiled because included by others
        # C_files.remove("POUS.c")
        if not C_files:
            self.logger.error(
                _("Error : At least one configuration and one resource must be declared in PLC !\n"))
            return False
        # transform those base names to full names with path
        C_files = list(map(
            lambda filename: os.path.join(buildpath, filename), C_files))

        # prepend beremiz include to configuration header
        H_files = [fname for fname in result.splitlines() if fname[
                                                             -2:] == ".h" or fname[-2:] == ".H"]
        H_files.remove("LOCATED_VARIABLES.h")
        H_files = list(map(
            lambda filename: os.path.join(buildpath, filename), H_files))
        for H_file in H_files:
            with open(H_file, 'r', encoding='utf-8') as original:
                data = original.read()
            with open(H_file, 'w', encoding='utf-8') as modified:
                modified.write('#include "beremiz.h"\n' + data)
        for r in self.res_files:
            res = os.path.join(buildpath, r + ".c")
            with open(res, 'r', encoding='utf-8') as original:
                data = original.read()
            with open(res, 'w', encoding='utf-8') as modified:
                modified.write('#ifdef USE_NO_IRQ\n' + data + "\n#endif\n")
        with open(os.path.join(buildpath, "POUS.c"), 'r', encoding='utf-8') as fc:
            data = fc.read()
        with open(os.path.join(buildpath, "POUS.c"), 'w', encoding='utf-8') as fd:
            fd.write('#include "POUS.h"\n' + data)
        with open(os.path.join(buildpath, "POUS.h"), 'r', encoding='utf-8') as fc:
            data = fc.read()
            data = data.replace('__DECLARE_DERIVED_TYPE(AXIS_REF,INT)', '#if 0\n__DECLARE_DERIVED_TYPE(AXIS_REF,INT)')
            data = data.replace('typedef __ARRAY_OF_INT_6 MC_INT_ARRAY;',
                                'typedef __ARRAY_OF_INT_6 MC_INT_ARRAY;\n#endif\n')
        with open(os.path.join(buildpath, "POUS.h"), 'w', encoding='utf-8') as fd:
            fd.write(data)
        self.logger.info(_("Extracting Located Variables...\n"))
        # Keep track of generated located variables for later use by
        # self._Generate_C
        self.PLCGeneratedLocatedVars = self.GetLocations()
        # Keep track of generated C files for later use by self.CTNGenerate_C
        self.PLCGeneratedCFiles = C_files
        # compute CFLAGS for plc
        self.plcCFLAGS = '"-I%s" -Wno-unused-function' % self.iec2c_cfg.getLibCPath()

        self.LastBuiltIECcodeDigest = self.IECcodeDigest

        for p in self.prg_c:
            h = os.path.join(buildpath, 'POUS.h')
            c = os.path.join(buildpath, 'POUS.c')
            with open(h, 'r', encoding='utf-8') as fh, open(c, 'rb') as fc:
                ch = fh.read()
                cc = fc.read()
            ch += '\n'.join(self.prg_c[p]['h'])
            cc += ('\n'.join(self.prg_c[p]['c'])).encode()
            with open(h, 'w', encoding='utf-8') as fh, open(c, 'wb') as fc:
                fh.write(ch)
                fc.write(cc.decode('utf-8', errors='ignore').encode('gbk', errors='ignore'))
        return True

    def GetBuilder(self):
        """
        Return a Builder (compile C code into machine code)
        """
        # Get target, module and class name
        targetname = self.GetTarget().getcontent().getLocalTag()
        targetclass = targets.GetBuilder(targetname)

        # if target already
        # if self._builder is None or not isinstance(self._builder, targetclass):
        # Get classname instance
        self._builder = targetclass(self)
        return self._builder

    #
    #
    #                C CODE GENERATION METHODS
    #
    #

    def CTNGenerate_C(self, buildpath, locations):
        """
        Return C code generated by iec2c compiler
        when _generate_softPLC have been called
        @param locations: ignored
        @return: [(C_file_name, CFLAGS),...] , LDFLAGS_TO_APPEND
        """

        return ([(C_file_name, self.plcCFLAGS)
                 for C_file_name in self.PLCGeneratedCFiles],
                "",  # no ldflags
                False, [])  # do not expose retreive/publish calls

    def ResetIECProgramsAndVariables(self):
        """
        Reset variable and program list that are parsed from
        CSV file generated by IEC2C compiler.
        """
        self._ProgramList = None
        self._VariablesList = None
        self._DbgVariablesList = None
        self._IECPathToIdx = {}
        self._Ticktime = 0
        self.TracedIECPath = []
        self.TracedIECTypes = []

    def GetIECProgramsAndVariables(self):
        """
        Parse CSV-like file  VARIABLES.csv resulting from IEC2C compiler.
        Each section is marked with a line staring with '//'
        list of all variables used in various POUs
        """
        if self._ProgramList is None or self._VariablesList is None:
            try:
                csvfile = os.path.join(self._getBuildPath(), "VARIABLES.csv")
                if not exists(csvfile): return
                # describes CSV columns
                ProgramsListAttributeName = ["num", "C_path", "type"]
                VariablesListAttributeName = [
                    "num", "vartype", "IEC_path", "C_path", "type", "derived", "retain"]
                self._ProgramList = []
                self._VariablesList = []
                self._DbgVariablesList = []
                self._IECPathToIdx = {}

                # Separate sections
                ListGroup = []
                for line in open(csvfile, 'r', encoding='utf-8').readlines():
                    strippedline = line.strip()
                    if strippedline.startswith("//"):
                        # Start new section
                        ListGroup.append([])
                    elif len(strippedline) > 0 and len(ListGroup) > 0:
                        # append to this section
                        ListGroup[-1].append(strippedline)

                # first section contains programs
                for line in ListGroup[0]:
                    # Split and Maps each field to dictionnary entries
                    attrs = dict(
                        list(zip(ProgramsListAttributeName, line.strip().split(';'))))
                    # Truncate "C_path" to remove conf an resources names
                    attrs["C_path"] = '__'.join(
                        attrs["C_path"].split(".", 2)[1:])
                    # Push this dictionnary into result.
                    self._ProgramList.append(attrs)

                # second section contains all variables
                config_FBs = {}
                Idx = 0
                for line in ListGroup[1]:
                    # Split and Maps each field to dictionnary entries
                    attrs = dict(
                        list(zip(VariablesListAttributeName, line.strip().split(';'))))
                    # Truncate "C_path" to remove conf an resources names
                    parts = attrs["C_path"].split(".", 2)
                    if len(parts) > 2:
                        config_FB = config_FBs.get(tuple(parts[:2]))
                        if config_FB:
                            parts = [config_FB] + parts[2:]
                            attrs["C_path"] = '.'.join(parts)
                        else:
                            attrs["C_path"] = '__'.join(parts[1:])
                    else:
                        attrs["C_path"] = '__'.join(parts)
                        if attrs["vartype"] == "FB":
                            config_FBs[tuple(parts)] = attrs["C_path"]
                    if attrs["vartype"] not in ["FB", "EXT", "TEMP"] and attrs["type"] in DebugTypesSize:
                        # Push this dictionnary into result.
                        self._DbgVariablesList.append(attrs)
                        # Fill in IEC<->C translation dicts
                        IEC_path = attrs["IEC_path"]
                        self._IECPathToIdx[IEC_path] = (Idx, attrs["type"])
                        # Ignores numbers given in CSV file
                        # Idx=int(attrs["num"])
                        # Count variables only, ignore FBs
                        Idx += 1
                    self._VariablesList.append(attrs)

                # third section contains ticktime
                if len(ListGroup) > 2:
                    self._Ticktime = int(ListGroup[2][0])

            except Exception:
                print(
                    _("Cannot open/parse VARIABLES.csv!\n"))
                print(traceback.format_exc())
                self.ResetIECProgramsAndVariables()
                return False

        return True

    def Generate_plc_debugger(self):
        """
        Generate trace/debug code out of PLC variable list
        """
        self.GetIECProgramsAndVariables()

        # prepare debug code
        variable_decl_array = []
        retain_indexes = []

        bofs = 0
        for i, v in enumerate(self._DbgVariablesList):
            sz = DebugTypesSize.get(v["type"], 0)
            variable_decl_array += [
                "{&(%(C_path)s), " % v +
                {
                    # "EXT": "%(type)s_P_ENUM",
                    "IN": "%(type)s_P_ENUM",
                    "MEM": "%(type)s_O_ENUM",
                    "OUT": "%(type)s_O_ENUM",
                    "VAR": "%(type)s_ENUM"
                }[v["vartype"]] % v +
                "}"]
            bofs += sz
            if v["retain"] == "1":
                retain_indexes.append("/* " + v["C_path"] + " */ " + str(i))
        debug_code = targets.GetCode("plc_debug") % {
            "buffer_size": bofs,
            "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;" %
                                                p for p in self._ProgramList]),
            "extern_variables_declarations": "\n".join([
                {
                    # "EXT": "extern __IEC_%(type)s_p %(C_path)s;",
                    "IN": "extern __IEC_%(type)s_p %(C_path)s;",
                    "MEM": "extern __IEC_%(type)s_p %(C_path)s;",
                    "OUT": "extern __IEC_%(type)s_p %(C_path)s;",
                    "VAR": "extern __IEC_%(type)s_t %(C_path)s;",
                    "FB": "extern       %(type)s   %(C_path)s;"
                }[v["vartype"]] % v
                for v in self._VariablesList if v["C_path"].find('.') < 0]),
            "variable_decl_array": ",\n".join(variable_decl_array),
            "retain_vardsc_index_array": ",\n".join(retain_indexes),
            "var_access_code": targets.GetCode("var_access")
        }

        return debug_code

    def Generate_plc_main(self):
        """
        Use confnodes layout given in LocationCFilesAndCFLAGS to
        generate glue code that dispatch calls to all confnodes
        """
        # filter location that are related to code that will be called
        # in retreive, publish, init, cleanup
        locstrs = list(map(lambda x: "_".join(list(map(str, x))),
                           [loc for loc, _Cfiles, DoCalls in
                            self.LocationCFilesAndCFLAGS if loc and DoCalls]))

        # Generate main, based on template
        if not self.BeremizRoot.getDisable_Extensions():
            plc_main_code = targets.GetCode("plc_main_head") % {
                "calls_prototypes": "\n".join([(
                                                       "int __init_%(s)s();\n" +
                                                       "void __cleanup_%(s)s(void);\n" +
                                                       "void __retrieve_%(s)s(void);\n" +
                                                       "void __publish_%(s)s(void);") % {'s': locstr} for locstr in
                                               locstrs]),
                "retrieve_calls": "\n    ".join([
                    "__retrieve_%s();" % locstr for locstr in locstrs]),
                "publish_calls": "\n    ".join([  # Call publish in reverse order
                    "__publish_%s();" % locstrs[i - 1] for i in range(len(locstrs), 0, -1)]),
                "init_calls": "\n    ".join([
                    "init_level=%d; " % (i + 1) +
                    "if((res = __init_%s())){" % locstr +
                    # "printf(\"%s\"); "%locstr + #for debug
                    "return res;}" for i, locstr in enumerate(locstrs)]),
                "cleanup_calls": "\n    ".join([
                    "if(init_level >= %d) " % i +
                    "__cleanup_%s();" % locstrs[i - 1] for i in range(len(locstrs), 0, -1)])
            }
        else:
            plc_main_code = targets.GetCode("plc_main_head") % {
                "calls_prototypes": "\n",
                "retrieve_calls": "\n",
                "publish_calls": "\n",
                "init_calls": "\n",
                "cleanup_calls": "\n"
            }
        # plc_main_code += targets.GetTargetCode(
        #     self.GetTarget().getcontent().getLocalTag())
        plc_main_code += targets.GetCode("plc_main_tail")
        return plc_main_code

    def _Build(self):
        self.AppFrame.Save()
        StartCoroutine(self._coBuild, self.AppFrame)

    async def _coBuild(self, model=None):
        try:
            """
            Method called by user to (re)build SoftPLC and confnode tree
            """

            if self.AppFrame is not None:
                self.AppFrame.ClearErrors()
                self._CloseView(self._IECCodeView)

            buildpath = self._getBuildPath()

            for root, dirs, files in os.walk(buildpath):
                for file in files:
                    path = os.path.join(root, file)
                    os.remove(path)

            # Eventually create build dir
            if not os.path.exists(buildpath):
                os.mkdir(buildpath)
            if self.AppFrame is not None:
                self.logger.flush()
            # Get current or fresh builder
            node_target = self.GetTarget().getcontent()
            target = None
            if node_target is not None:
                target = node_target.getLocalTag()
            board = self.GetChildByName('board_0')
            if board:
                node = board.board_class
                if (node_target is None) or (target not in node.envs):
                    self.logger.error(_("Please select target type in [ %s ]") % ",".join(node.envs))
                    return False
            self.logger.info(_("Start build in %s\n") % buildpath)

            # Generate SoftPLC IEC code
            IECGenRes = await self._Generate_SoftPLC()
            if self.AppFrame is not None:
                self.UpdateButtons()

            # If IEC code gen fail, bail out.
            if not IECGenRes:
                self.logger.error(_("PLC code generation failed !\n"))
                return False

            # Reset variable and program list that are parsed from
            # CSV file generated by IEC2C compiler.
            self.ResetIECProgramsAndVariables()

            # Collect platform specific C code
            # Code and other files from extension
            if not self._Generate_runtime():
                return False

            # Get current or fresh builder
            builder = self.GetBuilder()
            if builder is None:
                self.logger.error(_("Fatal : cannot get builder.\n"))
                return False

            # Build
            if not await builder.build(model):
                self.logger.error(_("C Build failed.\n"))
                self._builder = None
                return False
        except Exception as ex:
            # builder.ResetBinaryMD5()
            self.logger.error(str(ex))
            logger.debug(traceback.format_exc())
            self._builder = None
            return False

        self.logger.info(_("Successfully built.\n"))
        # Update GUI status about need for transfer
        if self._connector:
            await self.CompareLocalAndRemotePLC()
        self._builder = None
        return True

    def _Generate_runtime(self):
        buildpath = self._getBuildPath()

        # CTN code gen is expected AFTER Libraries code gen,
        # at least SVGHMI relies on it.

        # Generate C code and compilation params from liraries
        try:
            LibCFilesAndCFLAGS, LibLDFLAGS, LibExtraFiles = self.GetLibrariesCCode(
                buildpath)
        except UserAddressedException as e:
            self.logger.write_error(e.message)
            return False
        except Exception as ex:
            self.logger.error(
                _("Runtime library extensions C code generation failed !\n"))
            self.logger.error(str(ex))
            self.logger.debug(traceback.format_exc())
            return False

        # Generate C code and compilation params from liraries
        try:
            CTNLocationCFilesAndCFLAGS, CTNLDFLAGS, CTNExtraFiles, Libs = self._Generate_C(
                buildpath,
                self.PLCGeneratedLocatedVars)
        except UserAddressedException as e:
            self.logger.write_error(e.message)
            return False
        except Exception as ex:
            self.logger.error(
                _("Runtime IO extensions C code generation failed !\n"))
            self.logger.error(str(ex))
            logger.error(traceback.format_exc())
            return False

        # Extensions also need plcCFLAGS in case they include beremiz.h
        CTNLocationCFilesAndCFLAGS = [
            (loc, [
                (code, self.plcCFLAGS + " " + cflags)
                for code, cflags in code_and_cflags], do_calls)
            for loc, code_and_cflags, do_calls in CTNLocationCFilesAndCFLAGS]
        self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + CTNLocationCFilesAndCFLAGS
        self.LDFLAGS = CTNLDFLAGS + LibLDFLAGS
        self.Libs = Libs
        ExtraFiles = CTNExtraFiles + LibExtraFiles

        # Get temporary directory path
        extrafilespath = self._getExtraFilesPath()
        # Remove old directory
        if os.path.exists(extrafilespath):
            shutil.rmtree(extrafilespath)
        # Recreate directory
        os.mkdir(extrafilespath)
        # Then write the files
        for fname, fobject in ExtraFiles:
            fpath = os.path.join(extrafilespath, fname)
            open(fpath, "w", encoding='utf-8').write(fobject.read())
        # Now we can forget ExtraFiles (will close files object)
        del ExtraFiles

        # Header file for extensions
        open(os.path.join(buildpath, "beremiz.h"), "w", encoding='utf-8').write(
            targets.GetHeader())

        # Template based part of C code generation
        # files are stacked at the beginning, as files of confnode tree root
        c_source = [
            #  debugger code
            (self.Generate_plc_debugger, "plc_debugger.c", "Debugger"),
            # init/cleanup/retrieve/publish, run and align code
            (self.Generate_plc_main, "plc_main.c", "Common runtime")
        ]

        for generator, filename, name in c_source:
            try:
                # Do generate
                code = generator()
                if code is None:
                    raise Exception
                code_path = os.path.join(buildpath, filename)
                open(code_path, "w", encoding='utf-8').write(code)
                # Insert this file as first file to be compiled at root
                # confnode
                self.LocationCFilesAndCFLAGS[0][1].insert(
                    0, (code_path, self.plcCFLAGS))
            except Exception:
                self.logger.error(name + _(" generation failed !\n"))
                logger.error(traceback.format_exc())
                return False
        self.logger.info(_("C code generated successfully.\n"))
        return True

    def ShowError(self, logger, from_location, to_location):
        chunk_infos = self.GetChunkInfos(from_location, to_location)
        for infos, (start_row, start_col) in chunk_infos:
            row = 1 if from_location[0] < start_row else (
                    from_location[0] - start_row)
            col = 1 if (start_row != from_location[0]) else (
                    from_location[1] - start_col)
            start = (row, col)

            row = 1 if to_location[0] < start_row else (
                    to_location[0] - start_row)
            col = 1 if (start_row != to_location[0]) else (
                    to_location[1] - start_col)
            end = (row, col)

            if self.AppFrame is not None:
                self.AppFrame.ShowError(infos, start, end)

    _IECCodeView = None

    def _showIDManager(self):
        dlg = IDManager(self.AppFrame, self)
        dlg.ShowModal()
        # dlg.Destroy()

    def _showIECcode(self):
        self._OpenView("IEC code")

    _IECRawCodeView = None

    def _editIECrawcode(self):
        self._OpenView("IEC raw code")

    _ProjectFilesView = None

    def _OpenProjectFiles(self):
        self._OpenView("Project Files")

    _FileEditors = {}

    def _OpenFileEditor(self, filepath):
        self._OpenView(filepath)

    def _OpenView(self, name=None, onlyopened=False):
        if name == "IEC code":
            if self._IECCodeView is None:
                plc_file = self._getIECcodepath()
                win = wx.MDIChildFrame(self.AppFrame, -1, name)
                self._IECCodeView = IECCodeViewer(win, name, self.AppFrame, None, instancepath=name)
                win.Maximize()
                win.Show(True)
                self._IECCodeView.SetTextSyntax("ALL")
                self._IECCodeView.SetKeywords(IEC_KEYWORDS)
                try:
                    text = open(plc_file, encoding='utf-8').read()
                except Exception:
                    text = '(* No IEC code have been generated at that time ! *)'
                self._IECCodeView.SetText(text=text)
                self._IECCodeView.Editor.SetReadOnly(True)
                self._IECCodeView.SetIcon(GetBitmap("ST"))
                setattr(self._IECCodeView, "_OnClose", self.OnCloseEditor)
                win.Bind(wx.EVT_CLOSE, self._IECCodeView.onClose)

            if self._IECCodeView is not None:
                self.AppFrame.EditProjectElement(self._IECCodeView, name)

            return self._IECCodeView

        elif name == "IEC raw code":
            if self._IECRawCodeView is None:
                controler = MiniTextControler(self._getIECrawcodepath(), self)

                win = wx.MDIChildFrame(self.AppFrame, -1, name)
                self._IECRawCodeView = IECCodeViewer(win, "", self.AppFrame, controler, instancepath=name)
                self._IECRawCodeView.SetTextSyntax("ALL")
                self._IECRawCodeView.SetKeywords(IEC_KEYWORDS)
                asyncio.get_event_loop().run_until_complete(self._IECRawCodeView.RefreshView())
                self._IECRawCodeView.SetIcon(GetBitmap("ST"))
                setattr(self._IECRawCodeView, "_OnClose", self.OnCloseEditor)
                win.Bind(wx.EVT_CLOSE, self._IECRawCodeView.onClose)
                win.Maximize()
                win.Show(True)

            if self._IECRawCodeView is not None:
                self.AppFrame.EditProjectElement(self._IECRawCodeView, name)

            return self._IECRawCodeView

        elif name == "Project Files":
            win = None
            if self._ProjectFilesView is None:
                win = wx.MDIChildFrame(self.AppFrame, -1, name)
                self._ProjectFilesView = FileManagementPanel(win, self, name, self._getProjectFilesPath(), True)
                win.Bind(wx.EVT_CLOSE, self._ProjectFilesView.onClose)

                extensions = []
                for extension, _name, _editor in features.file_editors:
                    if extension not in extensions:
                        extensions.append(extension)
                self._ProjectFilesView.SetEditableFileExtensions(extensions)

            if win and self._ProjectFilesView is not None:
                self.AppFrame.EditProjectElement(self._ProjectFilesView, name)
                win.Maximize()
                win.Show(True)

            return self._ProjectFilesView

        elif name is not None and name.find("::") != -1:
            filepath, editor_name = name.split("::")
            if filepath not in self._FileEditors and os.path.isfile(filepath):
                file_extension = os.path.splitext(filepath)[1]

                editors = dict([(edit_name, edit_class)
                                for extension, edit_name, edit_class in features.file_editors
                                if extension == file_extension])

                if editor_name == "":
                    if len(editors) == 1:
                        editor_name = list(editors.keys())[0]
                    elif len(editors) > 0:
                        names = list(editors.keys())
                        dialog = wx.SingleChoiceDialog(
                            self.AppFrame,
                            _("Select an editor:"),
                            _("Editor selection"),
                            names,
                            wx.DEFAULT_DIALOG_STYLE | wx.OK | wx.CANCEL)
                        if dialog.ShowModal() == wx.ID_OK:
                            editor_name = names[dialog.GetSelection()]
                        # dialog.Destroy()

                if editor_name != "":
                    name = "::".join([filepath, editor_name])

                    editor = editors[editor_name]()
                    win = wx.MDIChildFrame(self.AppFrame, -1, name)
                    self._FileEditors[filepath] = editor(win, self, name, self.AppFrame)
                    self._FileEditors[filepath].SetIcon(GetBitmap("FILE"))
                    if isinstance(self._FileEditors[filepath], DebugViewer):
                        self._FileEditors[filepath].SetDataProducer(self)
                    win.Bind(wx.EVT_CLOSE, self._FileEditors[filepath].onClose)
                    win.Maximize()
                    win.Show(True)

            if filepath in self._FileEditors:
                editor = self._FileEditors[filepath]
                self.AppFrame.EditProjectElement(editor, editor.GetTagName())

            return self._FileEditors.get(filepath)
        else:
            return ConfigTreeNode._OpenView(self, self.CTNName(), onlyopened)

    def OnCloseEditor(self, view):
        ConfigTreeNode.OnCloseEditor(self, view)
        if self._IECCodeView == view:
            self._IECCodeView = None
        if self._IECRawCodeView == view:
            self._IECRawCodeView = None
        if self._ProjectFilesView == view:
            self._ProjectFilesView = None
        if view in list(self._FileEditors.values()):
            self._FileEditors.pop(view.GetFilePath())

    def _Clean(self):
        StartCoroutine(self._coClean, self.AppFrame)

    async def _coClean(self):
        self._CloseView(self._IECCodeView)
        try:
            if os.path.isdir(os.path.join(self._getBuildPath())):
                self.logger.info(_("Cleaning the build directory\n"))
                shutil.rmtree(os.path.join(self._getBuildPath()))
            else:
                self.logger.error(_("Build directory already clean\n"))
        except Exception as e:
            dialog = wx.MessageDialog(
                self.AppFrame,
                _("Cannot Clean Build directory.Please Manual remove Directoy src ."),
                style=wx.YES_DEFAULT | wx.CENTRE)
            dialog.ShowModal()
        try:
            if os.path.isdir(os.path.join(self.ProjectPath, ".platformio")):
                self.logger.info(_("Cleaning the Lib directory\n"))
                # shutil.rmtree(os.path.join(self.ProjectPath, ".platformio"))
                cmd = "rd /s /q %s" % os.path.join(self.ProjectPath, ".platformio")
                os.system(cmd)
            else:
                self.logger.error(_("Lib directory already clean\n"))
        except Exception as e:
            dialog = wx.MessageDialog(
                self.AppFrame,
                _("Cannot Clean Lib directory.Please Manual remove Directoy .platformio ."),
                style=wx.YES_DEFAULT | wx.CENTRE)
            dialog.ShowModal()
        # kill the builder
        self._builder = None
        await  self.CompareLocalAndRemotePLC()
        self.UpdateButtons()

    def _UpdateButtons(self):
        if self.AppFrame:
            StartCoroutine(self._coUpdateButtons, self.AppFrame)

    async def _coUpdateButtons(self):

        self.EnableMethod("_Clean", os.path.exists(self._getBuildPath()))
        self.ShowMethod("_showIECcode", os.path.isfile(self._getIECcodepath()))
        if self.AppFrame is not None and not await self.UpdateMethodsFromPLCStatus():
            self.AppFrame.RefreshStatusToolBar()

    def UpdateButtons(self):
        wx.CallAfter(self._UpdateButtons)

    async def UpdatePLCLog(self, log_count):
        if log_count and self.AppFrame is not None:
            await self.AppFrame.LogViewer.SetLogCounters(log_count)

    DefaultMethods = {
        "_Run": False,
        "_Stop": False,
        "_Transfer": False,
        "_Connect": True,
        "_Repair": False,
        "_Disconnect": False,
        '_Program': True,
        '_Ucopy': True
    }
    MethodsFromStatus = {
        Started: {"_Stop": True,
                  "_Transfer": False,
                  "_Connect": False,
                  "_Disconnect": True, '_Program': False
                  },
        Stopped: {"_Run": True,
                  "_Transfer": True,
                  "_Connect": False,
                  "_Disconnect": True,
                  "_Repair": True, '_Program': False
                  },
        Empty: {"_Transfer": True,
                "_Connect": False,
                "_Disconnect": True, '_Program': False
                },
        Broken: {"_Connect": True,
                 "_Repair": True,
                 "_Disconnect": False, '_Program': True},
        Disconnected: {"_Connect": True, "_Disconnect": False, '_Program': True},
    }

    async def UpdateMethodsFromPLCStatus(self):
        updated = False
        status = Disconnected
        if self._connector is not None:
            PLCstatus = await self._connector.Get_PLC_status()
            ## @interface GetPLCstatus���ݸ�ʽ(str,list(int*4))
            if PLCstatus is not None:
                status, log_count = PLCstatus
                # await self.UpdatePLCLog(log_count)
            if status == Disconnected:
                await self.CloseConnector(False)
                status = Disconnected
        if self.previous_plcstate != status:
            allmethods = self.DefaultMethods.copy()
            allmethods.update(
                self.MethodsFromStatus.get(status, {"_Stop": True,
                                                    "_Transfer": True,
                                                    "_Connect": False,
                                                    "_Disconnect": True, '_Program': False}))
            for method, active in allmethods.items():
                self.ShowMethod(method, active)
            self.previous_plcstate = status
            if self.AppFrame is not None:
                updated = True
                self.AppFrame.RefreshStatusToolBar()
                if status == Disconnected:
                    self.AppFrame.ConnectionStatusBar.SetStatusText(_(pb_state.Name(status)), 3)
                    self.AppFrame.ConnectionStatusBar.SetStatusText('', 4)
                else:
                    self.AppFrame.ConnectionStatusBar.SetStatusText(
                        _("Connected to URI: %s") % self.uri.strip(), 3)
                    self.AppFrame.ConnectionStatusBar.SetStatusText(_(pb_state.Name(status)), 4)
        return updated

    def ShowPLCProgress(self, status="", progress=0):
        '''状态栏进度条操作'''
        self.AppFrame.ProgressStatusBar.Show()
        self.AppFrame.ConnectionStatusBar.SetStatusText(_(status), 3)
        self.AppFrame.ProgressStatusBar.SetValue(progress)

    async def HidePLCProgress(self):
        # clear previous_plcstate to restore status
        # in UpdateMethodsFromPLCStatus()
        self.previous_plcstate = ""
        if self.AppFrame is not None:
            self.AppFrame.ProgressStatusBar.Hide()
        # await self.UpdateMethodsFromPLCStatus()

    async def PullPLCStatusProc(self):
        while self._connector:
            await self.UpdateMethodsFromPLCStatus()
            await asyncio.sleep(1)

    async def SnapshotAndResetDebugValuesBuffers(self):
        debug_status = Disconnected
        if self._connector is not None:
            debug_status, Traces = await self._connector.GetTraceVariables(self.DebugToken)
            # print [dict.keys() for IECPath, (dict, log, status, fvalue) in
            # self.IECdebug_datas.items()]
            if debug_status in [Started, STATE_POWER_FAULT] and len(Traces) > 0:
                for debug_tick, debug_buff in Traces:
                    debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes)
                    if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath):
                        for IECPath, values_buffer, value in zip(
                                self.TracedIECPath,
                                self.DebugValuesBuffers,
                                debug_vars):
                            IECdebug_data = self.IECdebug_datas.get(
                                IECPath, None)
                            if IECdebug_data is not None and value is not None:
                                forced = (IECdebug_data[2] == "Forced") \
                                         and (value is not None) and \
                                         (IECdebug_data[3] is not None)

                                if not IECdebug_data[4] and len(values_buffer) > 0:
                                    values_buffer[-1] = (value, forced)
                                else:
                                    values_buffer.append((value, forced))
                        self.DebugTicks.append(debug_tick)
                    else:
                        logger.error("变量数量不匹配 %d & %d" % (len(debug_vars or []), len(self.TracedIECPath or [])))

        buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers,
                                            [list() for dummy in range(len(self.TracedIECPath))])
        ticks, self.DebugTicks = self.DebugTicks, []
        return debug_status, ticks, buffers

    RegisterDebugVariableErrorCodes = {
        # Connector only can return None
        None: _("Debug: connection problem.\n"),
        # TRACE_LIST_OVERFLOW
        1: _("Debug: Too many variables traced. Max 1024.\n"),
        # FORCE_LIST_OVERFLOW
        2: _("Debug: Too many variables forced. Max 256.\n"),
        # FORCE_BUFFER_OVERFLOW
        3: _("Debug: Cumulated forced variables size too large. Max 1KB.\n"),
        # DEBUG_SUSPENDED
        4: _("Debug: suspended.\n")
    }

    def RegisterDebugVarToConnector(self):
        try:
            '''添加监控变量'''
            Idxs = []
            self.TracedIECPath = []
            self.TracedIECTypes = []
            # if not self._connector:
            #     await self._coConnect()
            if self._connector is not None and self.debug_status != Broken:
                # asyncio.run(self.IECdebug_lock.acquire())
                # logger.debug('IECdebug_lock get')
                IECPathsToPop = []
                for IECPath, data_tuple in self.IECdebug_datas.items():
                    WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple
                    if len(WeakCallableDict) == 0:
                        # Callable Dict is empty.
                        # This variable is not needed anymore!
                        IECPathsToPop.append(IECPath)
                    elif IECPath != "__tick__":
                        # Convert
                        Idx, IEC_Type = self._IECPathToIdx.get(
                            IECPath, (None, None))
                        if Idx is not None:
                            if IEC_Type in DebugTypesSize:
                                Idxs.append((Idx, IEC_Type, IECPath))
                            else:
                                self.logger.warning(
                                    _("Debug: Unsupported type to debug '%s'\n") % IEC_Type)
                        else:
                            self.logger.warning(
                                _("Debug: Unknown variable '%s'\n") % IECPath)
                for IECPathToPop in IECPathsToPop:
                    self.IECdebug_datas.pop(IECPathToPop)

                if Idxs:
                    Idxs.sort()
                    IdxsT = list(zip(*Idxs))
                    self.TracedIECPath = IdxsT[2]
                    self.TracedIECTypes = IdxsT[1]
                    self.DebugToken = loop.run_until_complete(
                        self._connector.SetTraceVariablesList(list(zip(*IdxsT[0:2]))))
                else:
                    self.TracedIECPath = []
                    loop.run_until_complete(self._connector.SetTraceVariablesList([]))
                    self.DebugToken = None
                    # self.debug_status, _debug_ticks, _buffers = await self.SnapshotAndResetDebugValuesBuffers()
                asyncio.create_task(self._coDispatchDebugValuesProc())
                # self.IECdebug_lock.release()
                # logger.debug('IECdebug_lock.release')
            self.DebugUpdatePending = False

        except Exception as ex:
            logger.error(traceback.format_exc())

    async def coRegisterForceVarToConnector(self):
        '''添加强制变量'''
        Idxs = []
        if self._connector is not None:  # and self.debug_status != "Broken":
            # await self.IECdebug_lock.acquire()
            # logger.debug('IECdebug_lock get')
            IECPathsToPop = []
            for IECPath, data_tuple in self.IECdebug_force.items():
                force, value = data_tuple
                if force != 'Forced':
                    IECPathsToPop.append(IECPath)
                # Convert
                Idx, IEC_Type = self._IECPathToIdx.get(
                    IECPath, (None, None))
                if Idx is not None:
                    if IEC_Type in DebugTypesSize:
                        Idxs.append((Idx, IEC_Type, value))
                    else:
                        self.logger.warning(
                            _("Debug: Unsupported type to debug '%s'\n") % IEC_Type)
                else:
                    self.logger.warning(
                        _("Debug: Unknown variable '%s'\n") % IECPath)

            if Idxs:
                Idxs.sort()
                IdxsT = list(zip(*Idxs))
                await self._connector.SetForceVariablesList(list(zip(*IdxsT[0:3])))
            for IECPathToPop in IECPathsToPop:
                self.IECdebug_force.pop(IECPathToPop)
            # self.IECdebug_lock.release()
            # logger.debug('IECdebug_lock.release')

    def IsPLCStarted(self):
        return self.previous_plcstate == Started

    def AppendDebugUpdate(self):
        if not self.DebugUpdatePending:
            wx.CallAfter(self.RegisterDebugVarToConnector)
            self.DebugUpdatePending = True

    def GetDebugIECVariableType(self, IECPath):
        _Idx, IEC_Type = self._IECPathToIdx.get(IECPath, (None, None))
        return IEC_Type

    prompt = False

    async def SubscribeDebugIECVariable(self, IECPath, callableobj, buffer_list=False):
        """
        Dispatching use a dictionnary linking IEC variable paths
        to a WeakKeyDictionary linking
        weakly referenced callables
        """
        if not self._IECPathToIdx:
            self.GetIECProgramsAndVariables()
        if IECPath != "__tick__" and IECPath not in self._IECPathToIdx:
            logger.error(f"{IECPath} not found")
            return None

        if len(self.IECdebug_datas) >= self.reg_var_nrs:
            if not self.prompt:
                dialog = wx.MessageDialog(self.AppFrame, _("监控变量超过限值"), style=wx.OK | wx.ICON_ERROR | wx.CENTRE)
                if dialog.ShowModal() == wx.ID_OK:
                    self.prompt = True
            return None

        # logger.debug('IECdebug_lock to')
        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        # If no entry exist, create a new one with a fresh WeakKeyDictionary
        IECdebug_data = self.IECdebug_datas.get(IECPath, None)
        if IECdebug_data is None:
            IECdebug_data = [
                WeakKeyDictionary(),  # Callables
                [],  # Data storage [(tick, data),...]
                "Registered",  # Variable status
                None,
                buffer_list]  # Forced value
            self.IECdebug_datas[IECPath] = IECdebug_data
        else:
            IECdebug_data[4] |= buffer_list

        IECdebug_data[0][callableobj] = buffer_list

        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')
        self.AppendDebugUpdate()

        return IECdebug_data[1]

    def UnsubscribeDebugIECVariable(self, IECPath, callableobj):
        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        IECdebug_data = self.IECdebug_datas.get(IECPath, None)
        if IECdebug_data is not None:
            IECdebug_data[0].pop(callableobj, None)
            if len(IECdebug_data[0]) == 0:
                self.IECdebug_datas.pop(IECPath)
            else:
                IECdebug_data[4] = reduce(
                    lambda x, y: x | y,
                    list(IECdebug_data[0].values()),
                    False)
        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')

        # self.AppendDebugUpdate()

    async def UnsubscribeAllDebugIECVariable(self):
        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        self.IECdebug_datas = {}
        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')

        self.AppendDebugUpdate()

    async def ForceDebugIECVariable(self, IECPath, fvalue):
        if IECPath not in self.IECdebug_datas:
            return

        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        IECdebug_data = self.IECdebug_datas.get(IECPath, None)
        IECdebug_data[2] = "Forced"
        IECdebug_data[3] = fvalue
        # If no entry exist, create a new one with a fresh WeakKeyDictionary
        IECdebug_data = self.IECdebug_force.get(IECPath, None)
        if not IECdebug_data:
            self.IECdebug_force[IECPath] = [None, None]
            IECdebug_data = self.IECdebug_force.get(IECPath, None)
        IECdebug_data[0] = "Forced"
        IECdebug_data[1] = fvalue

        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')

        StartCoroutine(self.coRegisterForceVarToConnector, self.AppFrame)

    async def ReleaseDebugIECVariable(self, IECPath):
        if IECPath not in self.IECdebug_datas:
            return

        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        IECdebug_data = self.IECdebug_datas.get(IECPath, None)
        IECdebug_data[2] = "Registered"
        IECdebug_data[3] = None
        # If no entry exist, create a new one with a fresh WeakKeyDictionary
        IECdebug_data = self.IECdebug_force.get(IECPath, None)
        if not IECdebug_data:
            self.IECdebug_force[IECPath] = [None, None]
            IECdebug_data = self.IECdebug_force.get(IECPath, None)
        IECdebug_data[0] = "Registered"
        IECdebug_data[1] = None

        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')

        StartCoroutine(self.coRegisterForceVarToConnector, self.AppFrame)

    def CallWeakcallables(self, IECPath, function_name, *cargs):
        data_tuple = self.IECdebug_datas.get(IECPath, None)
        if data_tuple is not None:
            WeakCallableDict, _data_log, _status, _fvalue, buffer_list = data_tuple
            # data_log.append((debug_tick, value))
            for weakcallable, buffer_list in WeakCallableDict.items():
                Function = getattr(weakcallable, function_name, None)
                if Function is not None:
                    try:
                        if buffer_list:
                            Function(*cargs)
                        else:
                            Function(*tuple([lst[-1] for lst in cargs]))
                    except:
                        pass

    def GetTicktime(self):
        return self._Ticktime

    def RemoteExec(self, script, **kwargs):
        if self._connector is None:
            return -1, "No runtime connected!"
        return self._connector.RemoteExec(script, **kwargs)

    def soem_cmd(self, cmd, **kwargs):
        if self._connector is None:
            return -1, "No runtime connected!"
        return loop.run_until_complete(self._connector.soem_cmd(cmd, **kwargs))

    def SiiRead(self, slave):
        if self._connector is None:
            return -1, "No runtime connected!"
        cmd = pb_soem_cmd()
        cmd.cmd = pb_soem_cmd_type.soem_get_sii
        cmd.slave_id = slave + 1
        res = loop.run_until_complete(self._connector.soem_cmd(cmd))
        if res.msg == "OK":
            return res.object.soem_sii.sii_data
        return None

    def SiiWrite(self, slave, sii):
        if self._connector is None:
            return -1, "No runtime connected!"
        cmd = pb_soem_cmd()
        cmd.cmd = pb_soem_cmd_type.soem_set_sii
        cmd.slave_id = slave + 1
        cmd.sii = sii
        res = loop.run_until_complete(self._connector.soem_cmd(cmd))
        return res

    def DispatchDebugValuesProc(self, event):
        if self.AppFrame:
            asyncio.create_task(self._coDispatchDebugValuesProc())
        event.Skip()

    async def _coDispatchDebugValuesProc(self):
        # if len(self.TracedIECPath) == 0:
        #     return
        if not self._connector:
            return
        start_time = time.time()
        # await self.IECdebug_lock.acquire()
        # logger.debug('IECdebug_lock get')
        debug_status, debug_ticks, buffers = await self.SnapshotAndResetDebugValuesBuffers()
        # self.IECdebug_lock.release()
        # logger.debug('IECdebug_lock.release')

        if len(self.TracedIECPath) == len(buffers):
            for IECPath, values in list(zip(self.TracedIECPath, buffers)):
                if len(values) > 0:
                    self.CallWeakcallables(IECPath, "NewValues", debug_ticks, values)
            if len(debug_ticks) > 0:
                self.CallWeakcallables("__tick__", "NewDataAvailable", debug_ticks)

        delay = time.time() - start_time
        next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
        if self.DispatchDebugValuesTimer is not None:
            self.DispatchDebugValuesTimer.Start(int(next_refresh * 1000), oneShot=True)

    def KillDebugThread(self):
        if self.DispatchDebugValuesTimer is not None:
            self.DispatchDebugValuesTimer.Stop()
        self.IECdebug_force = {}

    async def _connect_debug(self):
        self.previous_plcstate = None
        if self.AppFrame:
            self.AppFrame.ResetGraphicViewers()
        self.debug_status = Started
        self.RegisterDebugVarToConnector()
        if self.DispatchDebugValuesTimer is not None:
            self.DispatchDebugValuesTimer.Start(
                int(REFRESH_PERIOD * 1000), oneShot=True)
            self.logger.info(_("debugger started."))

    def _Run(self):
        StartCoroutine(self._coRun, self.AppFrame)

    async def _coRun(self):
        """
        Start PLC
        """
        if self._connector and self.GetIECProgramsAndVariables():
            await self._connector.cmd(CB_PLC_START)
            self.logger.info(_("Starting PLC\n"))
            await self._connect_debug()
        else:
            self.logger.error(_("Couldn't start PLC !\n"))
        StartCoroutine(self.UpdateMethodsFromPLCStatus, self.AppFrame)

    def _Stop(self):
        StartCoroutine(self._coStop, self.AppFrame)

    async def _coStop(self):
        res = False
        if self._connector is not None:
            sta = await self._connector.cmd(CB_PLC_STOP)
            if sta != 0:
                self.logger.error(_("Couldn't stop PLC !\n"))
            else:
                self.logger.info(_("PLC stopped.\n"))
                res = True

        # debugthread should die on his own
        # self.KillDebugThread()

        StartCoroutine(self.UpdateMethodsFromPLCStatus, self.AppFrame)
        return res

    async def OpenConnector(self, connector, update_status=True):
        if await connector.init():
            self._connector = connector
            if self.AppFrame is not None:
                self.AppFrame.LogViewer.SetLogSource(connector)
            StartCoroutine(self.PullPLCStatusProc, self.AppFrame)

    async def CloseConnector(self, update_status=True):
        logger.debug('CloseConnector begin')
        if self._connector is not None:
            # await self._connector.close_async()
            self.logger.info(_("debugger already disconnected."))
        self._connector = None
        if self.AppFrame and update_status:
            StartCoroutine(self.UpdateMethodsFromPLCStatus, self.AppFrame)
        logger.debug('CloseConnector end')

    def _Connect(self):
        assert self.AppFrame
        StartCoroutine(self._coConnect, self.AppFrame)

    async def _coConnect(self):
        logger.debug("_coConnect enter")
        '''
        连接控制器
        1. 通过mDNS探寻网络中可连接的控制器
        1. @todo:扫描串口上连接的控制器
        1. @todo:连接物联网控制器
        '''
        # don't accept re-connetion if already connected
        rte_latest = None
        self.logger.flush()
        mdns = 'pac'
        if self._connector is not None:
            self.logger.error(_("Already connected. Please disconnect\n"))
            return

        arduino_wait = 10
        try:
            builder = self.GetBuilder()
            if builder is None:
                self.logger.error(_("Fatal : cannot get builder.\n"))
                return False
            await builder.debug_start()

            board = self.GetChildByName('board_0')
            if board:
                node = board.board_class
                if not node.model:
                    self.logger.error(_("PLC Board Unknown"))
                self.rts = node.prog_boot
                self.dtr = node.prog_reset
                arduino_wait = board.current_rte.boot_delay
                rte_latest = board.get_latest_rte_version()
        except Exception as ex:
            logger.debug(traceback.format_exc())
            self.logger.error(str(ex))
            return

        # Get connector uri
        self.uri = None
        # if uri is empty launch discovery dialog
        try:
            # Launch Service Discovery dialog
            dialog = UriEditor(self.AppFrame, self, uri=self.uri,
                               service_type='_pac._udp.local.')
            answer = dialog.ShowModal()
            self.uri = str(dialog.GetURI())
            dialog.close()
            # dialog.Destroy()
            # Nothing choosed or cancel button
            if self.uri is None or answer == wx.ID_CANCEL:
                self.logger.error(_("Connection canceled!\n"))
                return
        except Exception as ex:
            self.logger.error(_("Local service discovery failed!\n"))
            logger.error(traceback.format_exc())
            self.uri = None
            return

        # self.BeremizRoot.setURI_location(self.uri)
        self.logger.info(_('Try to connect %s ...\n') % self.uri)
        # if self._View is not None:
        #     await self._View.RefreshView()
        if self.AppFrame is not None:
            self.AppFrame.RefreshTitle()
            self.AppFrame.RefreshFileMenu()
            self.AppFrame.RefreshEditMenu()
            self.AppFrame.RefreshPageTitles()
        # Get connector from uri
        try:
            scheme, location = self.uri.split("://")
            if scheme in ['HTTP', 'network']:
                self.ip, port = location.split(':')
            await self.OpenConnector(
                ConnectorFactory(self.uri, self, dtr=self.dtr, rts=self.rts, arduino_wait=arduino_wait))
        except SerialException as ex:
            self.logger.error(_("Serial connection failed! %s\n" % (str(ex))))
            logger.error(traceback.format_exc())
            return
        except Exception as e:
            self.logger.error(
                _("Exception while connecting to '{uri}': {ex}\n").format(
                    uri=self.uri, ex=e))
            logger.debug(traceback.format_exc())

        # Did connection success ?
        if self._connector is None:
            # Oups.
            self.logger.error(_("Connection failed to %s!\n") % self.uri)
        else:
            try:
                def ver_cmp(latest, c):
                    latest = latest.split('.')
                    c = c.split('.')
                    if int(latest[0]) > int(c[0]):
                        return True
                    if int(latest[1]) > int(c[1]):
                        return True
                    if int(latest[2]) > int(c[2]):
                        return True
                    return False

                ver = await self._connector.fm_protobuf(pb_cmd_type.read_rte_const, 0)
                if ver:
                    rte = "%d.%d.%d" % (ver['major'], ver['minor'], ver['build'])
                    self.logger.info(_('Firmware sn:%X , Version:%s') % (ver['sn'], rte))
                    if rte_latest and ver_cmp(rte_latest, rte):
                        self.logger.info("固件最新版本是%s,请更新固件" % rte_latest)
                    # self.reg_var_nrs = (ver[6] - 6) / 2
                else:
                    self.logger.error(_("Firmware version unknown"))
                await self.UpdateMethodsFromPLCStatus()
                await self.CompareLocalAndRemotePLC()
                # Init with actual PLC status and print it
                await self.UpdateMethodsFromPLCStatus()

                if self.DebugAvailable() and self.GetIECProgramsAndVariables():
                    await self._connect_debug()
                else:
                    self.logger.warning(
                        _("Debug does not match PLC - stop/transfert/start to re-enable\n"))
            except Exception as ex:
                self._connector = None
                self.logger.error(
                    _("Connection failed to {uri}! state={state} .\n").format(uri=self.uri, state=str(ex)))
                logger.debug(traceback.format_exc())

    async def CompareLocalAndRemotePLC(self):
        '''通过MD5比较应用是否匹配'''
        if self._connector is None:
            self.logger.error(_("Not connecte PLC.\n"))
            return False
        while self.previous_plcstate in ['Booting', 'Disconnected']:
            self.logger.info(_("\rWait for PLC start..."))
            await self.UpdateMethodsFromPLCStatus()
        builder = self.GetBuilder()
        if builder is None:
            self.logger.error(_("Not get builder.\n"))
            return
        MD5 = builder.GetBinaryMD5()
        # Check remote PLC PLC correspondance to that md5
        rmd5 = await self._connector.MatchMD5(MD5)
        if rmd5:
            self.logger.info(
                _("Latest build matches with connected PLC.\n"))
            self.ProgramTransferred()
            return True
        else:
            self.logger.error(_("Latest build does not match with connected PLC.\n"))
            self.EnableMethod("_Transfer", False)
        return False

    def _Disconnect(self):
        StartCoroutine(self._coDisconnect, self.AppFrame)

    async def _coDisconnect(self):
        await self.CloseConnector()
        if self._connector:
            await self._connector.close_async()
        self.IECdebug_force = {}

    def _Transfer(self):
        self.logger.info("Transfer PLC...\n")
        StartCoroutine(self._coTransfer, self.AppFrame)

    async def _coTransfer(self):
        logger.debug('_coTransfer begin')
        self.KillDebugThread()
        if self.IsPLCStarted():
            dialog = wx.MessageDialog(
                self.AppFrame,
                _("Cannot transfer while PLC is running. Stop it now?"),
                style=wx.YES_NO | wx.CENTRE)
            if dialog.ShowModal() == wx.ID_YES and not await self._coStop():
                return
        builder = self.GetBuilder()
        if builder is None:
            self.logger.error(_("Fatal : cannot get builder.\n"))
            return False

        # recover md5 from last build
        MD5 = builder.GetBinaryMD5()

        # Check if md5 file is empty : ask user to build PLC
        if MD5 is None:
            self.logger.error(
                _("Failed : Must build before transfer.\n"))
            return False

        # Compare PLC project with PLC on target
        rMD5 = await self._connector.MatchMD5(MD5)
        if rMD5:
            self.logger.info(
                _("Latest build already matches current PLC. Transfering anyway...\n"))

        # purge any non-finished transfer
        # note: this would abord any runing transfer with error

        target = self.GetTarget().getcontent().getLocalTag()
        # self._Disconnect()
        await self.HidePLCProgress()
        self.previous_plcstate = Broken
        await self._connector.PurgeBlobs()
        try:
            object_path = builder.GetBinaryPath()
            if not exists(object_path):
                raise Exception(_("PLC program %s not exist!Please Build first.\n" % object_path))
            if not self.GetIECProgramsAndVariables():
                self.logger.error('请编译当前应用程序')
            boardNode = self.GetChildByName('board_0')
            # if boardNode:
            #     Board = boardNode.BoardRoot.getBoard()[0]
            #     if getattr(Board, 'getIP', None):
            #         IP = Board.getIP()
            #         if IP:
            #             ips = IP.split('.')
            #             data = bytes(
            #                 [int(ips[0]), int(ips[1]), int(ips[2]), int(ips[3]), 255, 255, 255, 0, int(ips[0]),
            #                  int(ips[1]),
            #                  int(ips[2]), 1])
            #             await self._connector.SetIP(data)
            #             self.logger.info(_("PLC ip set:%s\n") % (IP))

            self.logger.info(_("PLC program transfered start...\n"))
            p1 = None
            try:
                await  self._connector.cmd(CB_PLC_REPAIR, 5)
                # if target == 'Win32_pio':
                #     self.logger.write(f"本地加载 {object_path}")
                #     p1 = await self._connector.SetPlcFile(object_path)
                # elif boardNode.board_class.upload_protocol == 'tftp':
                #     p1 = await self._connector.PostFile(object_path, boardNode.board_class.dest_prog)
                if boardNode.board_class.upload_protocol == 'ftp':
                    p1 = await self._connector.ftp_upload(object_path, boardNode.board_class.dest_prog)
                # elif boardNode.board_class.upload_protocol == 'XYmodem':
                #     p1 = await self._connector.xymodem(object_path, boardNode.board_class.dest_prog)
                elif boardNode.board_class.upload_protocol == 'Firmata' or 'Firmata' in boardNode.board_class.upload_protocol:
                    p1 = await  self._connector.send_file(object_path, boardNode.board_class.dest_prog,
                                                          boardNode.board_class.app_place,
                                                          boardNode.board_class.app_address,
                                                          boardNode.board_class.app_max_size)
                else:
                    raise Exception(_("unknown method_download %s") % boardNode.board_class.upload_protocol)
            except RuntimeError as e:
                logger.error(traceback.format_exc())
                self.logger.error(str(e))
            if p1:
                self.logger.info(_("PLC program transfered successfully.\n"))
            else:
                self.logger.error(_("PLC program transfered Failed!\n"))
            p2 = p3 = True
            # p2 = await self._connector.PostFile(builder._GetMD5FileName(), 'lastbuildPLC.md5')
            # if p2:
            #     self.logger.info(_("PLC MD5 transfered successfully.\n"))
            # else:
            #     self.logger.error(_("PLC MD5 transfered Failed!\n"))
            # p3 = await self._connector.PostFile(builder._GetProgFlagFileName(), 'prog.flag')
            # if p3:
            #     self.logger.info(_("PLC FLAG transfered successfully.\n"))
            # else:
            #     self.logger.error(_("PLC FLAG transfered Failed!\n"))
            # if self._connector.NewPLC(MD5, object_blob, extrafiles):
            if p1 and p2 and p3:
                await self.UnsubscribeAllDebugIECVariable()
                self.ProgramTransferred()
                self.AppFrame.CloseObsoleteDebugTabs()
                self.AppFrame.RefreshPouInstanceVariablesPanel()
                self.AppFrame.LogViewer.ResetLogCounters()
                await  self._connector.cmd(CB_PLC_LOAD)
                await asyncio.sleep(2)
                loop.run_until_complete(self._connect_debug())
                self.previous_plcstate = None
                self.logger.info(_("PLC installed successfully.\n"))
            else:
                self.logger.error(_("PLC installed Failed.\n"))
            # else:
            #     self.logger.error(_("PLC couldn't be installed\n"))

        except Exception as e:
            logger.debug(traceback.format_exc())
            self.logger.error(
                _("Exception while put file to '{uri}': {ex}\n").format(
                    uri=self.uri, ex=e))

        StartCoroutine(self.UpdateMethodsFromPLCStatus, self.AppFrame)
        self.logger.info("end.\n")
        logger.debug('_coTransfer end.')

    def _Repair(self):
        StartCoroutine(self._coRepair, self.AppFrame)

    async def _coRepair(self):
        dialog = wx.MessageDialog(
            self.AppFrame,
            _('Delete target PLC application?'),
            _('Repair'),
            wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
        answer = dialog.ShowModal()
        dialog.Destroy()
        if answer == wx.ID_YES:
            await self._connector.cmd(CB_PLC_REPAIR)

    StatusMethods = [
        {
            "bitmap": "Build",
            "name": _("Build"),
            "tooltip": _("Build project into build folder"),
            "method": "_Build"
        },
        {
            "bitmap": "Clean",
            "name": _("Clean"),
            "tooltip": _("Clean project build folder"),
            "method": "_Clean",
            "enabled": False,
        },
        {
            "bitmap": "Run",
            "name": _("Run"),
            "tooltip": _("Start PLC"),
            "method": "_Run",
            "shown": False,
        },
        {
            "bitmap": "Stop",
            "name": _("Stop"),
            "tooltip": _("Stop Running PLC"),
            "method": "_Stop",
            "shown": False,
        },
        {
            "bitmap": "Connect",
            "name": _("Connect"),
            "tooltip": _("Connect to the target PLC"),
            "method": "_Connect"
        },
        {
            "bitmap": "Transfer",
            "name": _("Transfer"),
            "tooltip": _("Transfer PLC"),
            "method": "_Transfer",
            "shown": False,
        },
        {
            "bitmap": "Disconnect",
            "name": _("Disconnect"),
            "tooltip": _("Disconnect from PLC"),
            "method": "_Disconnect",
            "shown": False,
        }, {
            "bitmap": "Repair",
            "name": _("Repair"),
            "tooltip": _("Repair broken PLC"),
            "method": "_Repair",
            "shown": False,
        },
        {
            "bitmap": "flash",
            "name": _("Program"),
            "tooltip": _("Program PLC with Serial"),
            "method": "_Program",
            "shown": True,
        },
        {
            "bitmap": "udisk",
            "name": _("Copy Firmware"),
            "tooltip": _("copy firmware to UDisk"),
            "method": "UCopy",
            "shown": True,
        }, {
            "bitmap": "IDManager",
            "name": _("ID Manager"),
            "tooltip": _("Manage secure connection identities"),
            "method": "_showIDManager",
        },
        {
            "bitmap": "ShowIECcode",
            "name": _("Show code"),
            "tooltip": _("Show IEC code generated by PLCGenerator"),
            "method": "_showIECcode",
            "shown": False,
        },
    ]

    ConfNodeMethods = [
        {
            "bitmap": "editIECrawcode",
            "name": _("Raw IEC code"),
            "tooltip": _("Edit raw IEC code added to code generated by PLCGenerator"),
            "method": "_editIECrawcode"
        },
        {
            "bitmap": "ManageFolder",
            "name": _("Project Files"),
            "tooltip": _("Open a file explorer to manage project files"),
            "method": "_OpenProjectFiles"
        },
    ]

    def EnableMethod(self, method, value):
        for d in self.StatusMethods:
            if d["method"] == method:
                d["enabled"] = value
                return True
        return False

    def ShowMethod(self, method, value):
        for d in self.StatusMethods:
            if d["method"] == method:
                d["shown"] = value
                return True
        return False

    def CallMethod(self, method):
        for d in self.StatusMethods:
            if d["method"] == method and d.get("enabled", True) and d.get("shown", True):
                getattr(self, method)()

    def getSerials(self):
        serial = []
        board = self.GetChildByName('board_0')
        if board:
            node = board.board_class
            serial = node.serial_list
        return serial

    def getCanList(self):
        canlist = []
        board = self.GetChildByName('board_0')
        if board:
            node = board.board_class
            canlist = node.can_list
        return canlist

    def check_var_name(self, name):
        if '__' in name:
            raise Exception(_("Don't use '__'!"))

    def check_location(self, location):
        ##todo:��λ���������ظ�
        pass

    def OpenXMLFile(self, filepath):
        self.Project, error = plcopen.LoadProject(filepath)
        if self.Project is None:
            logger.debug(_("Project file syntax error:\n\n") + error)
            return _("Project file syntax error:\n\n") + error
        self.SetFilePath(filepath)
        self.CreateProjectBuffer(True)
        self.ProgramChunks = []
        self.ProgramOffset = 0
        self.NextCompiledProject = self.Copy(self.Project)
        self.CurrentCompiledProject = None
        self.Buffering = False
        self.CurrentElementEditing = None
        return error

    def _Program(self):
        StartCoroutine(self._coProgram, self.AppFrame)

    async def _coProgram(self):
        dialog = ProgEditor(self.AppFrame, self)
        dialog.ShowModal()
        # dialog.close()
        # dialog.Destroy()

    def SetEditedResourceInfos(self, tagname, tasks, instances):
        """
        Args:
            tagname:
            tasks:
            instances:
        """
        resource = self.GetEditedElement(tagname)
        if resource is not None:
            resource.settask([])
            resource.setpouInstance([])
            task_list = {}
            for task in tasks:
                new_task = PLCOpenParser.CreateElement("task", "resource")
                resource.appendtask(new_task)
                new_task.setname(task["Name"])
                if task["Triggering"] == "Interrupt":
                    new_task.setsingle(task["Single"])
                #                result = duration_model.match(task["Interval"]).groups()
                #                if reduce(lambda x, y: x or y != None, result):
                #                    values = []
                #                    for value in result[:-1]:
                #                        if value != None:
                #                            values.append(int(value))
                #                        else:
                #                            values.append(0)
                #                    if result[-1] is not None:
                #                        values.append(int(float(result[-1]) * 1000))
                #                    new_task.setinterval(datetime.time(*values))
                elif task["Triggering"] == "Cyclic":
                    new_task.setinterval(task["Interval"])
                elif task["Triggering"] == "Timer":
                    new_task.settimer(task["Timer"])
                    new_task.setinterval(task["Interval"])
                elif task["Triggering"] == "Exti":
                    new_task.setexti(task["Exti"])
                    new_task.setrise(task["Rise"])
                    new_task.setfall(task["Fall"])
                new_task.setpriority(int(task["Priority"]))
                if task["Name"] != "":
                    task_list[task["Name"]] = new_task
            for instance in instances:
                task = task_list.get(instance["Task"])
                if task is not None:
                    new_instance = PLCOpenParser.CreateElement("pouInstance", "task")
                    task.appendpouInstance(new_instance)
                else:
                    new_instance = PLCOpenParser.CreateElement("pouInstance", "resource")
                    resource.appendpouInstance(new_instance)
                new_instance.setname(instance["Name"])
                new_instance.settypeName(instance["Type"])

    def GetEditedResourceInfos(self, tagname, debug=False):
        """
        Args:
            tagname:
            debug:
        """
        resource = self.GetEditedElement(tagname, debug)
        if resource is not None:
            tasks = resource.gettask()
            instances = resource.getpouInstance()
            tasks_data = []
            instances_data = []
            for task in tasks:
                new_task = {}
                new_task["Name"] = task.getname()
                single = task.getsingle()
                if single is not None:
                    new_task["Single"] = single
                else:
                    new_task["Single"] = ""
                interval = task.getinterval()
                timer = task.gettimer()
                exti = task.getexti()
                rise = task.getrise()
                fall = task.getfall()
                if interval is not None:
                    # text = ""
                    # if interval.hour != 0:
                    #     text += "%dh"%interval.hour
                    # if interval.minute != 0:
                    #     text += "%dm"%interval.minute
                    # if interval.second != 0:
                    #     text += "%ds"%interval.second
                    # if interval.microsecond != 0:
                    #     if interval.microsecond % 1000 != 0:
                    #         text += "%.3fms"%(float(interval.microsecond) / 1000)
                    #     else:
                    #         text += "%dms"%(interval.microsecond / 1000)
                    # new_task["Interval"] = text
                    new_task["Interval"] = interval
                else:
                    new_task["Interval"] = ""
                new_task["Timer"] = timer or ""
                new_task["Exti"] = exti or ""
                new_task["Rise"] = rise or ""
                new_task["Fall"] = fall or ""
                if single is not None and interval is None:
                    new_task["Triggering"] = "Interrupt"
                elif single is None and timer is not None:
                    new_task["Triggering"] = "Timer"
                elif interval is not None and single is None:
                    new_task["Triggering"] = "Cyclic"
                elif interval is None and single is None and timer is None and (rise is not None or fall is not None):
                    new_task["Triggering"] = "Exti"
                else:
                    new_task["Triggering"] = ""
                new_task["Priority"] = str(task.getpriority())
                tasks_data.append(new_task)
                for instance in task.getpouInstance():
                    new_instance = {}
                    new_instance["Name"] = instance.getname()
                    new_instance["Type"] = instance.gettypeName()
                    new_instance["Task"] = task.getname()
                    instances_data.append(new_instance)
            for instance in instances:
                new_instance = {}
                new_instance["Name"] = instance.getname()
                new_instance["Type"] = instance.gettypeName()
                new_instance["Task"] = ""
                instances_data.append(new_instance)
            return tasks_data, instances_data

    def GenerateProgramC(self, filepath=None, logger=None):
        """
        Args:
            filepath:
            logger:
        """
        errors = []
        warnings = []
        if self.Project is not None:
            try:
                generator = ProgramGenerator(self, self.Project, errors, warnings, build_dir=self._getBuildPath())

                def log(txt):
                    logger.info("    " + txt + "\n")

                generator.GenerateProgram(log)
                self.ProgramChunks = generator.GetGeneratedProgram()
                self.NextCompiledProject = self.Copy(self.Project)
                self.prg_c = generator.prg_c
                self.res_files = generator.res_files
                program_text = "".join([item[0] for item in self.ProgramChunks])
                if filepath is not None:
                    programfile = open(filepath, "w", encoding='utf-8')
                    programfile.write(program_text)
                    programfile.close()
                    self.ProgramFilePath = filepath

                    with open(filepath.replace("generated_plc.st", "retain.csv"), 'w', encoding='utf-8') as fp:
                        for line in generator.retainVars:
                            fp.write(','.join(line))
                            fp.write('\n')
                return program_text, errors, warnings
            except Exception as ex:
                logger.error(traceback.format_exc())
                errors.append(ex)
        else:
            errors.append("No project opened")
        return "", errors, warnings

    def sortVariblesH(self, h_file):
        """
        Args:
            h_file:
        """
        with open(h_file, 'r', encoding='utf-8') as fp:
            lines = fp.readlines()
            lines = set(lines)
        lines = ''.join(lines)
        with open(h_file, 'w', encoding='utf-8') as fp:
            fp.write(lines)

    def UCopy(self):
        dialog = UCopyEditor(self.AppFrame, self)
        dialog.ShowModal()

    def PackageProject(self):
        fi = self.CTNPackage()
        fi.append(os.path.join(self.ProjectPath, 'plc.xml'))
        fi.append(os.path.join(self.ProjectPath, 'beremiz.xml'))
        PackageDialog(self.AppFrame, self, fi).ShowModal()

    async def soem_scan(self):
        if self._connector is None:
            await self._coConnect()
        if self._connector:
            return await self._connector.soem_scan()
        return -1, []

    @property
    def current_rte(self):
        board = self.GetChildByName('board_0')
        if board:
            return board.current_rte
        return None
